package bma.common.netty.webserver.file;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;

import org.jboss.netty.channel.Channel;
import org.jboss.netty.channel.ChannelFuture;
import org.jboss.netty.channel.ChannelFutureListener;
import org.jboss.netty.channel.ChannelFutureProgressListener;
import org.jboss.netty.channel.DefaultFileRegion;
import org.jboss.netty.channel.FileRegion;
import org.jboss.netty.handler.codec.http.DefaultHttpResponse;
import org.jboss.netty.handler.codec.http.HttpHeaders;
import org.jboss.netty.handler.codec.http.HttpHeaders.Names;
import org.jboss.netty.handler.codec.http.HttpMethod;
import org.jboss.netty.handler.codec.http.HttpRequest;
import org.jboss.netty.handler.codec.http.HttpResponse;
import org.jboss.netty.handler.codec.http.HttpResponseStatus;
import org.jboss.netty.handler.codec.http.HttpVersion;
import org.jboss.netty.handler.ssl.SslHandler;
import org.jboss.netty.handler.stream.ChunkedFile;

import bma.common.langutil.ai.stack.AIStack;
import bma.common.langutil.core.VPath;
import bma.common.netty.webserver.RequestContext;
import bma.common.netty.webserver.RequestDispatcher;
import bma.common.netty.webserver.WebServerUtil;

public class RequestProcessorStaticFile extends RequestDispatcher {

	final org.slf4j.Logger log = org.slf4j.LoggerFactory
			.getLogger(RequestProcessorStaticFile.class);

	private String root;
	private int cacheSeconds = 60;

	public String getRoot() {
		return root;
	}

	public void setRoot(String root) {
		this.root = root;
	}

	public int getCacheSeconds() {
		return cacheSeconds;
	}

	public void setCacheSeconds(int cacheSeconds) {
		this.cacheSeconds = cacheSeconds;
	}

	private static final String LTAG = "StaticFile";

	@Override
	public boolean dispatch(AIStack<Boolean> stack, RequestContext ctx)
			throws Exception {

		String path = ctx.getPath();
		HttpRequest request = ctx.getRequest();

		final File file = sanitizeUri(path);
		if (file == null) {
			if (log.isDebugEnabled()) {
				log.debug("{} sanitizeUri fail", request.getUri());
			}
			WebServerUtil
					.endWithStatus(LTAG, ctx, HttpResponseStatus.FORBIDDEN);
			return true;
		}
		if (file.isHidden() || !file.exists()) {
			if (log.isDebugEnabled()) {
				log.debug("can't access - {}", file);
			}
			return false;
		}
		if (!file.isFile()) {
			if (log.isDebugEnabled()) {
				log.debug("can't access dir - {}", file);
			}
			WebServerUtil
					.endWithStatus(LTAG, ctx, HttpResponseStatus.FORBIDDEN);
			return true;
		}

		if (log.isDebugEnabled()) {
			log.debug("access - {}", file);
		}

		if (request.getMethod() != HttpMethod.GET) {
			if (log.isDebugEnabled()) {
				log.debug("only accept GET method - {}", request.getMethod());
			}
			WebServerUtil.endWithStatus(LTAG, ctx,
					HttpResponseStatus.METHOD_NOT_ALLOWED);
			return true;
		}

		// Cache Validation
		String ifModifiedSince = request.getHeader(Names.IF_MODIFIED_SINCE);
		if (ifModifiedSince != null && ifModifiedSince.length() != 0) {
			SimpleDateFormat dateFormatter = new SimpleDateFormat(
					WebServerUtil.HTTP_DATE_FORMAT, Locale.US);
			Date ifModifiedSinceDate = dateFormatter.parse(ifModifiedSince);

			// Only compare up to the second because the datetime format we send
			// to the client does
			// not have milliseconds
			long ifModifiedSinceDateSeconds = ifModifiedSinceDate.getTime() / 1000;
			long fileLastModifiedSeconds = file.lastModified() / 1000;
			if (ifModifiedSinceDateSeconds == fileLastModifiedSeconds) {
				if (log.isDebugEnabled()) {
					log.debug("not modified - {}", file);
				}
				WebServerUtil.endWithStatus(LTAG, ctx,
						HttpResponseStatus.NOT_MODIFIED);
				return true;
			}
		}

		RandomAccessFile raf;
		try {
			raf = new RandomAccessFile(file, "r");
		} catch (FileNotFoundException fnfe) {
			WebServerUtil.endWithNotFound(LTAG, ctx);
			return true;
		}
		long fileLength = raf.length();

		HttpResponse response = new DefaultHttpResponse(HttpVersion.HTTP_1_1,
				HttpResponseStatus.OK);
		HttpHeaders.setContentLength(response, fileLength);
		WebServerUtil.setContentTypeHeader(response, file.getPath());
		WebServerUtil.setDateAndCacheHeaders(response, file.lastModified(),
				cacheSeconds);

		// Write the initial line and the header.
		Channel ch = ctx.getChannel();
		ch.write(response);

		// Write the content.
		ChannelFuture writeFuture;
		if (ch.getPipeline().get(SslHandler.class) != null) {
			// Cannot use zero-copy with HTTPS.
			writeFuture = ch.write(new ChunkedFile(raf, 0, fileLength, 8192));
		} else {
			// No encryption - use zero-copy.
			final FileRegion region = new DefaultFileRegion(raf.getChannel(),
					0, fileLength);
			writeFuture = ch.write(region);
			writeFuture.addListener(new ChannelFutureProgressListener() {
				public void operationComplete(ChannelFuture future) {
					region.releaseExternalResources();
				}

				public void operationProgressed(ChannelFuture future,
						long amount, long current, long total) {
					if (log.isDebugEnabled()) {
						log.debug("{}: {} / {} (+{})", new Object[] { file,
								current, total, amount });
					}
				}
			});
		}

		// Decide whether to close the connection or not.
		if (!HttpHeaders.isKeepAlive(request)) {
			// Close the connection when the whole content is written out.
			writeFuture.addListener(ChannelFutureListener.CLOSE);
		}
		WebServerUtil.endWith200(LTAG, ctx);
		return true;
	}

	protected File sanitizeUri(String uri) throws IOException {
		// Decode the path.
		try {
			uri = URLDecoder.decode(uri, "UTF-8");
		} catch (UnsupportedEncodingException e) {
			try {
				uri = URLDecoder.decode(uri, "ISO-8859-1");
			} catch (UnsupportedEncodingException e1) {
				throw new Error();
			}
		}

		// Convert file separators.
		// uri = uri.replace('/', File.separatorChar);

		// Simplistic dumb security check.
		// You will have to do something serious in the production environment.
		if (uri.contains(File.separator + '.')
				|| uri.contains('.' + File.separator) || uri.startsWith(".")
				|| uri.endsWith(".")) {
			return null;
		}

		// Convert to absolute path.
		VPath path = VPath.create(uri).parse();
		return new File(this.root, path.toString(false));
	}
}